/******************************************************************************/
package jamie.org.sil.www.usxConverter;

import java.io.*;
import java.util.regex.*;

import jamie.org.sil.www.usxUtils.*;
import java.util.HashMap;
import java.util.Stack;


/******************************************************************************/
/**
 * A class to handle conversion to OTML.
 * <p>
 *
 * If it comes as any consolation, most of the complexity here arises because
 * we have to avoid certain tag combinations which OTML cannot cope with.
 * 
 * @author ARA "Jamie" Jamieson
**/

public class UsxConverterToOtml extends UsxConverterTo
{
  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                                 Public                                 **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  /**
     Constructor.
  */

  public UsxConverterToOtml () throws IOException, ConverterException
  {
    super();
    initialiseFeatures();
    initialiseGeneral();
  }

  
  /****************************************************************************/
  /** General start-of-tag process, called after every end tag. */
  
  @Override public void afterUsxEndTag (String tagName,
                                        String style,
                                        int level,
                                        UsxParser.ElementDetails eltDetails,
                                        int destination) throws Exception
  {
    popDest();
    if (m_ContextualSuppressions.suppresses(tagName, style, "//")) --m_SuppressDoubleSlashes;
    if (m_ContextualSuppressions.needToSuppress(tagName, style)) OutputManager.instance().setIgnoreData(destination, false);
    if (!"hash_text".equals(tagName)) m_ParsedTextOutputterStack.pop();
    super.afterUsxEndTag(tagName, style, level, eltDetails, destination);
  }
  
  
  /****************************************************************************/
  /** General start-of-tag process, called after every end tag. */
  
  @Override public void beforeUsxEndTag (String tagName,
                                         String style,
                                         int level,
                                         UsxParser.ElementDetails eltDetails,
                                         int destination) throws Exception
  {
    if ("para".equals(tagName)) OutputManager.instance().removeTrailingStringFromBufferRepeated(" ");
  }
  
  
  /****************************************************************************/
  /** General start-of-tag process, called before every start tags. */
  
  @Override public void beforeUsxStartTag (String tagName,
                                           String style,
                                           int level,
                                           UsxParser.ElementDetails eltDetails,
                                           int destination) throws Exception
  {
    /**************************************************************************/
    super.beforeUsxStartTag(tagName, style, level, eltDetails, destination);
    
    
    
    /**************************************************************************/
    /* Check if the context requires us to skip this tag.  If it does, attempt
     * to write a space to the given destination anyway, in case by skipping
     * we end up by having two things abutting.
     * 
     * Note that I make the assumption that if the tag needs to be skipped, so
     * will _all_ of its contents.*/

    if (m_ContextualSuppressions.needToSuppress(tagName, style))
    {
      if (okToWriteSpace()) writeToGivenDest(destination, m_Space);
      OutputManager.instance().setIgnoreData(destination, true);
    }
 
 
    
    /**************************************************************************/
    /* Within the range of certain tags, we can't cope with the // which gives
     * rise to a newline. */
    
    if (m_ContextualSuppressions.suppresses(tagName, style, "//")) ++m_SuppressDoubleSlashes;
    
    
    
    /**************************************************************************/
    /* Make sure we use the correct stringifier. */
    
    if (!"hash_text".equals(tagName))
    {
      if ("rem".equals(style) || "restore".equals(style) || !Config.instance().getGenerateLinksForAllScriptureRefs())
        m_ParsedTextOutputterStack.push(null); // Avoid attempting to find Bible refs in these.
      else
        m_ParsedTextOutputterStack.push(-1 == C_StylesNeedingXbReferences.indexOf("." + style + ".") ?
                                     m_ParsedTextOutputter_VernaculaNotDefaulted :
                                       m_RefsAsXbOutputter);
    }
    
    
    
    /**************************************************************************/
    /* Force subsequent output to the correct destination. */
    
    pushDest(destination);
  }
  
  
  /****************************************************************************/
  /**
   * Carries out termination processing.  Specifically, runs over all basic
   * scripture output files and checks to see if they have corresponding
   * Footnotes files.  If they do not, removes the PublisherNotes directive
   * from the scripture file.  Also copies any graphics files from the
   * Peripherals folder to the output folder.
   * 
   * @throws IOException 
   */
  
  @Override public void terminate () throws IOException
  {
    /**************************************************************************/
    String folderName = Config.instance().getOutputFolderPath();
    String fileList = Utils.getAllMatchingFilesFromFolder(folderName, "*", false, ";");
    String extension = Config.instance().getOutputFileExtension();


    
    
    /**************************************************************************/
    if (0 != fileList.length())
    {
      String[] files = fileList.split("\\;");
      for (int i = 0; i < files.length; ++i)
        if (-1 != files[i].indexOf(extension))
          removeReferenceToPublisherNotesIfNecessary(folderName, files[i]);
    }
 
    
    
   /**************************************************************************/
   copyGraphics();
 }

  
    


  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                     Specific tag handling routines                     **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  
  /****************************************************************************/
  /* Generated internally by the Xerces parser to flag text.  We need to
     perform common basic processing on the text a) handling USX-things
     like "<<" to match opening quotes; and b) doing any tidying needed
     by OliveTree itself.  Then we need to convert embedded references
     into an appropriate form. */

  @Override public void start_hash_text (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    /**************************************************************************/
    m_LengthOfMostRecentHashText = 0;
    String text = eltDetails.getNodeValue();

    

    /**************************************************************************/
    /* If the text has leading spaces, writing out at most one of them, and
       only if we're permitted to do so. */

    if (text.matches("^\\p{Z}")) { if (okToWriteSpace()) writeToCurrentDest(m_Space); }
    text = text.replaceAll("^\\p{Z}+", "");



    /**************************************************************************/
    /* In some contexts, the double slashes which signify a forced new line
       have to be suppressed. */

    if (m_SuppressDoubleSlashes > 0) text = text.replaceAll("//", m_Space);
    if (0 == text.length()) return;
    


    /**************************************************************************/
    /* Handle translations like those needed for USFM quote marks etc. */

    text = translateText(text);
    if (0 == text.length()) return;



    /**************************************************************************/
    /* Bit fiddly, this -- and I'm not too sure I can make sense of
       it (nor indeed whether it is needed, strictly speaking).

       I presume it is just about possible that the input may contain
       a few special character entities, in decimal format -- &#dddd;.
       Unfortunately that's not valid in OTML -- we need &udddd;
       apparently (and we need it in hex).  In fact, Xerces returns
       the strings in hex -- except that I _believe_ hex is supposed
       to come out as &#xdddd; and Xerces omits the "x" -- so it looks
       like decimal, but isn't.  Anyway, the purpose of the processing
       below is to identify character entities and convert them to the
       form needed by OliveTree. */

    Matcher matcher = C_CharacterEntityPattern.matcher(text);
    StringBuffer buf = new StringBuffer();
    int endPos = -1;

    while (matcher.find())
    {
      String x = matcher.group();
      x = x.substring(2);
      x = x.substring(0, x.length() - 1);
      matcher.appendReplacement(buf, "&u" + String.format("%4X", (Integer.parseInt(x, 10))) + ";");
      endPos = matcher.end();
    }

    if (endPos >= 0) text = buf + text.substring(endPos);



    /**************************************************************************/
    /* Unlikely, I guess, but the "//" procesing may have introduced <br />s
       which we need to cater for. */

    text = text.replaceAll("\\p{Z}*<br />\\p{Z}*", "<br />");
    text = text.replaceAll("(<br />)+", "<br />");
    text = text.replaceAll("\\p{Z}+", m_Space);



    /**************************************************************************/
    /* If we're processing scripture output, there's no need to worry about
     * handling cross-references, because there won't be any.  (And in fact,
     * it's best to avoid such processing anyway, because I have come across
     * one vernacular where numbers (such as 40 days and 40 nights) were
     * actually given as numerals, rather than text, which confused things.
     */
    
    if (UsxTagDetails.Canonicity.C_Canonical != m_CanonicityStack.peek())
    {
      ParsedTextOutputter outputter = m_ParsedTextOutputterStack.peek();
      if (null != outputter)
      {
        ParsedText parsedText =
          BibleReferenceParser.instance().parse(text, BibleDetails.instance().getAbbreviatedVernacularName_E(Bookmark.instance().getUbsAbbreviatedName()), NumeralProcessor.instance());
        text = outputter.toString(parsedText);
      }
    }

    
    
    /**************************************************************************/
    m_LengthOfMostRecentHashText = text.length();
    if (0 != m_LengthOfMostRecentHashText) writeToCurrentDest(text);
  }
  
  @Override public void end_hash_text (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
  }





  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                              Protected                                 **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  /* Handle the output for the end of a standard USX tag -- ie output either
     "p" or "span", along with the relevant class. */
  
  @Override protected void processUsxTagEnd (String tagName,
                                             String style,
                                             int level,
                                             UsxParser.ElementDetails eltDetails,
                                             int destination) throws Exception
  {
    String key = UsxParser.makeKey(tagName, style, level);
    if (m_SimpleTags.containsKey(key))
    {
      TagPair p = m_SimpleTags.get(key);
      if (null == p) return;
      
      String s = p.getClosingTag();
      if (null == s)
        setOutputToBitBucket(false);
      else
      {
        // See declaration of C_HeaderPattern for details of what's going on here.
        Matcher matcher = C_HeaderPatternClose.matcher(s);
        if (matcher.find())
        {
          String x = m_PendingHeaders.pop();
          if (0 != x.length()) writeToCurrentDest(s);
        }
        else
	  writeToCurrentDest(s);
      }

    }
  }

  
  /****************************************************************************/
  /* Handle the output for the start of a standard USX tag -- ie output either
     "p" or "span", along with the relevant class. */
  
  @Override protected void processUsxTagStart (String tagName,
                                               String style,
                                               int level,
                                               UsxParser.ElementDetails eltDetails,
                                               int destination) throws Exception
  {
    String key = UsxParser.makeKey(tagName, style, level);
    if (m_SimpleTags.containsKey(key))
    {
      TagPair p = m_SimpleTags.get(key);
      if (null == p)
      {
	LogManager.instance().warning("No processing defined for tag " + key);
        return;
      }
      
      String s = p.getOpeningTag();
      if (null == s)
        setOutputToBitBucket(true);
      else
      {
        /**********************************************************************/
        /* There are special comments which support the proofer.  We probably
         * don't want them in a production run, because they make the files
         * appreciably bigger. */
        
        if (!m_IncludeHooksForProofer) s = s.replaceAll("<comment>[^>]+</comment>", "");
        
        
        
        /**********************************************************************/
        /* See declaration of C_HeaderPattern for details of what's going on
           here. */
        
        Matcher matcher = C_HeaderPatternOpen.matcher(s);
        if (matcher.find())
        {
          if (!m_PendingHeaders.isEmpty())
          {
            String x = m_PendingHeaders.pop();
            if (0 != x.length())
            {
              writeToCurrentDest(x);
              m_PendingHeaders.push("");
            }
          }
          
          m_PendingHeaders.push(p.getClosingTag());
        }

        
 
        /**********************************************************************/
	writeToCurrentDest(s);
      }
    }
  }

  
  
  
  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                                Private                                 **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  /* Initialises the features list. */

  private void initialiseFeatures () throws IOException
  {
    FeatureManager features = FeatureManager.instance();
    features.setOptionListForOutputOverallDisposition("MultipleFiles");
    features.setOptionListForOutputFootnotesDisposition("Combined");
    features.setOptionListForOutputIntroductionDisposition("InSituInTextFile");
    features.setOptionListForOutputCrossReferencesDisposition("WithFootnotes");
    features.setOptionListForOutputEndnotesDisposition("WithFootnotes");
    features.setOptionListForOutputScriptureTextDisposition("Combined");
    features.setAcceptNotesDirectivesFromBookAssembly(false);
    features.setAcceptPeripheralsDirectivesFromBookAssembly(false);
    features.setAcceptScriptureDirectivesFromBookAssembly(true);
    features.setAcceptTocDirectivesFromBookAssembly(false);
    features.setSupportsToc(false);
    Config.instance().setOutputFootnotesFileName(Config.instance().getIso() + "-Footnotes." + Config.instance().getOutputFileExtension());
    Config.instance().setOutputScriptureTextFileName(Config.instance().getIso() + "." + Config.instance().getOutputFileExtension());
    Config.instance().setOutputIntroductionDisposition("InSituInTextFile"); // Force intro into scripture file.
    features.validateConfigurationParameters(-1);
  }


  /****************************************************************************/
  /** Carries out general initialisation. */

  private void initialiseGeneral () throws IOException, ConverterException
  {
    m_Parser = new UsxParser(this);
    m_FootnoteMarkerManager = new FootnoteMarkerManager(NumeralProcessor.instance());
    m_FootnoteMarkerManager.setPrefix("<sup>");
    m_FootnoteMarkerManager.setSuffix("</sup>");

    BookAssembly.instance().setPrefixesAndSuffixes(null, null, null, null);
    
    BookAssembly.instance().setHeaderAndTrailerWriters(m_HeaderAndTrailerWriterScripture,
                                                       m_HeaderAndTrailerWriterNotes, 
                                                       null,
                                                       null);
    initialiseConversionTable();

    m_ParsedTextOutputter_VernaculaNotDefaulted.setLanguage(BibleReferenceSupport.Language.C_Vernacular);
    m_ParsedTextOutputter_VernaculaNotDefaulted.setLength(BibleReferenceSupport.Length.C_AsSupplied);


    m_Space = Config.instance().getVernacularSpaceCharacter();
    m_IncludeHooksForProofer = Config.instance().getIncludeHooksForProofer();
    //m_IncludeHooksForProofer = true; // $$$
  }


  /****************************************************************************/
  /* Initialises the basic table used for tag conversions.
   * 
   * A note on <br /> processing ...
   * 
   * This is, to say the least, horrendously complicated.  To begin with,
   * OliveTree doesn't always seem to respect br's -- to some extent it
   * seems to depend upon what OliveTree tag you had immediately beforehand.
   * This means that if you want to be guaranteed to have n blank lines, you
   * really need to include n+1 br's (and then have to accept that unless you
   * want to go to a further level of sophistication beyond what we have here,
   * you may sometimes end up with more than n blanks).
   * 
   * This is further complicated in the case of heading tags (h2, h3, etc),
   * because of the behaviour you need.  A major heading always wants a lot
   * of space before it.  A minor heading wants perhaps less space, but it too
   * normally wants _some_ space ... unless it immediate follows another
   * heading, in which case you probably want no space at all.  Unfortunately
   * (or maybe fortunately), OliveTree doesn't generate any space at all as
   * a result of hitting a heading tag, so it's down to you to simulate the
   * space you need by using br's.
   * 
   * To complicate things still further, in the past we decided we needed
   * processing in place to _prevent_ too many consecutive br's from coming
   * out, and since I'm not sure whether we still want this, I've left it
   * in place.
   * 
   * To accommodate all of this, I have several flavours of br in what follows.
   * Plain br is output except after hr, br or p. headingBr is output
   * as br, but only if we have not just had a closing tag for a header.
   */
  
  private void initialiseConversionTable ()
  {
    /**************************************************************************/
    String C_LineSep = MyOutputStream.C_PseudoLineSeparatorAsString;


    
    /**************************************************************************/
    /* User-defined stuff. */
    
    m_SimpleTags.put("start_userDefinedPara_bra_introductoryMaterial", new TagPair("", ""));
    m_SimpleTags.put("start_userDefinedPara_ket_introductoryMaterial", new TagPair("", ""));
    m_SimpleTags.put("start_userDefinedPara_bra_ul", new TagPair("<ul>", ""));
    m_SimpleTags.put("start_userDefinedPara_ket_ul", new TagPair("", "</ul>"));
    
    
    
    /**************************************************************************/
    /* USFM tags which are not defined in USX. */

    m_SimpleTags.put("br", null); // Not defined in USX?
    m_SimpleTags.put("conc", null); // Not defined in USX?
    m_SimpleTags.put("cov", null); // Not defined in USX?
    m_SimpleTags.put("glo", null); // Not defined in USX?
    m_SimpleTags.put("idx", null); // Not defined in USX?
    m_SimpleTags.put("intro", null); // Not defined in USX?
    m_SimpleTags.put("maps", null); // Not defined in USX?
    m_SimpleTags.put("ph", null); // Not defined in USX?
    m_SimpleTags.put("ph1", null); // Not defined in USX?
    m_SimpleTags.put("ph2", null); // Not defined in USX?
    m_SimpleTags.put("ph3", null); // Not defined in USX?
    m_SimpleTags.put("phi", null); // Not defined in USX?
    m_SimpleTags.put("pr", null); // Not defined in USX?
    m_SimpleTags.put("pref", null); // Not defined in USX?
    m_SimpleTags.put("ps", null); // Not defined in USX?
    m_SimpleTags.put("psi", null); // Not defined in USX?
    m_SimpleTags.put("pub", null); // Not defined in USX?
    m_SimpleTags.put("pubinfo", null); // Not defined in USX?
    m_SimpleTags.put("spine", null); // Not defined in USX?
    m_SimpleTags.put("sts", null); // Not defined in USX?
    m_SimpleTags.put("toc", null); // Not defined in USX?
    m_SimpleTags.put("tr1", null); // Not defined in USX?
    m_SimpleTags.put("tr2", null); // Not defined in USX?
    m_SimpleTags.put("wr", null); // Not defined in USX?
    m_SimpleTags.put("xtSee", null); // Not defined in USX?
    m_SimpleTags.put("xtSeeAlso", null); // Not defined in USX?
    m_SimpleTags.put("zpa-d", null); // Not defined in USX?
    m_SimpleTags.put("zpa-xb", null); // Not defined in USX?
    m_SimpleTags.put("zpa-xc", null); // Not defined in USX?
    m_SimpleTags.put("zpa-xv", null); // Not defined in USX?



    /**************************************************************************/
    /* Tags for which we have no defined conversion to OTML. */
    
    m_SimpleTags.put("char_addpn", null); // For chinese words to be dot underline & underline.
    m_SimpleTags.put("char_dc", null); // Deuterocanonical/LXX additions or insertions in the Protocanonical text.
    m_SimpleTags.put("char_efm", null); // Reference to caller of previous footnote in a study Bible.
    m_SimpleTags.put("char_fdc", null); // Footnote text, applies to Deuterocanon only.
    m_SimpleTags.put("char_fl", null); // A footnote label text item, for marking or "labelling" the type or alternate translation being provided in the note.
    m_SimpleTags.put("char_fm", null); // Reference to caller of previous footnote.
    m_SimpleTags.put("char_fp", null); // A footnote additional paragraph marker.
    m_SimpleTags.put("char_ior", null); // Introduction references range for outline entry; for marking references separately.
    m_SimpleTags.put("char_iqt", null); // For quoted scripture text appearing in the introduction.
    m_SimpleTags.put("char_nd", null); // For name of deity.
    m_SimpleTags.put("char_ndx", null); // A subject index text item.
    m_SimpleTags.put("char_no", null); // A character style, use normal text.
    m_SimpleTags.put("char_ord", null); // For the text portion of an ordinal number.
    m_SimpleTags.put("char_pn", null); // For a proper name.
    m_SimpleTags.put("char_pro", null); // For indicating pronunciation in CJK texts.
    m_SimpleTags.put("char_qac", null); // Poetry text, Acrostic markup of the first character of a line of acrostic poetry.
    m_SimpleTags.put("char_qt", null); // For Old Testament quoted text appearing in the New Testament.
    m_SimpleTags.put("char_sls", null); // To represent where the original text is in a secondary language or from an alternate text source.
    m_SimpleTags.put("char_tc1", null); // A table cell item, column 1.
    m_SimpleTags.put("char_tc2", null); // A table cell item, column 1.
    m_SimpleTags.put("char_tc3", null); // A table cell item, column 1.
    m_SimpleTags.put("char_tc4", null); // A table cell item, column 1.
    m_SimpleTags.put("char_tcr1", null); // A table cell item, column 1, right aligned.
    m_SimpleTags.put("char_tcr2", null); // A table cell item, column 1, right aligned.
    m_SimpleTags.put("char_tcr3", null); // A table cell item, column 1, right aligned.
    m_SimpleTags.put("char_tcr4", null); // A table cell item, column 1, right aligned.
    m_SimpleTags.put("char_th1", null); // A table heading, column 1.
    m_SimpleTags.put("char_th2", null); // A table heading, column 1.
    m_SimpleTags.put("char_th3", null); // A table heading, column 1.
    m_SimpleTags.put("char_th4", null); // A table heading, column 1.
    m_SimpleTags.put("char_thr1", null); // A table heading, column 1, right aligned.
    m_SimpleTags.put("char_thr2", null); // A table heading, column 1, right aligned.
    m_SimpleTags.put("char_thr3", null); // A table heading, column 1, right aligned.
    m_SimpleTags.put("char_thr4", null); // A table heading, column 1, right aligned.
    m_SimpleTags.put("char_vp", null); // Published verse marker - this is a verse marking that would be used in the published text.
    m_SimpleTags.put("char_wg", null); // A Greek Wordlist text item.
    m_SimpleTags.put("char_wh", null); // A Hebrew wordlist text item.
    m_SimpleTags.put("char_xdc", null); // Cross-reference target reference(s), Deuterocanon only.
    m_SimpleTags.put("char_xk", null); // A cross reference keyword.
    m_SimpleTags.put("char_xq", null); // A cross-reference quotation from the scripture text.
    m_SimpleTags.put("note_fe", null); // Endnote position in main text.
    m_SimpleTags.put("para_cd", null); // Chapter Description (Publishing option D, e.g. in Russian Bibles).
    m_SimpleTags.put("para_cl", null); // Chapter label used for translations that add a word such as "Chapter".
    m_SimpleTags.put("para_cls", null); // Closure of an Epistle.
    m_SimpleTags.put("para_cp", null); // Published chapter number .
    m_SimpleTags.put("para_ib", null); // Introduction blank line.
    m_SimpleTags.put("para_iex", null); // Introduction explanatory or bridge text (e.g. explanation of missing book in Short Old Testament).
    m_SimpleTags.put("para_ili", null); // A list entry, level 1 (if single level).
    m_SimpleTags.put("para_ili1", null); // A list entry, level 1 (if multiple levels).
    m_SimpleTags.put("para_ili2", null); // A list entry, level 2.
    m_SimpleTags.put("para_im", null); // Introduction prose paragraph, with no first line indent (may occur after poetry).
    m_SimpleTags.put("para_imi", null); // Introduction prose paragraph text, indented, with no first line indent.
    m_SimpleTags.put("para_imq", null); // Introduction prose paragraph, quote from the body text, with no first line indent.
    m_SimpleTags.put("para_ipi", null); // Introduction prose paragraph, indented, with first line indent.
    m_SimpleTags.put("para_ipq", null); // Introduction prose paragraph, quote from the body text.
    m_SimpleTags.put("para_ipr", null); // Introduction prose paragraph, right aligned.
    m_SimpleTags.put("para_iq", null); // Introduction poetry text, level 1 (if single level).
    m_SimpleTags.put("para_iq1", null); // Introduction poetry text, level 1 (if multiple levels).
    m_SimpleTags.put("para_iq2", null); // Introduction poetry text, level 2 .
    m_SimpleTags.put("para_iq3", null); // Introduction poetry text, level 3.
    m_SimpleTags.put("para_k1", null); // Concordance main entry text or keyword, level 1.
    m_SimpleTags.put("para_k2", null); // Concordance main entry text or keyword, level 2.
    m_SimpleTags.put("para_lit", null); // For a comment or note inserted for liturgical use.
    m_SimpleTags.put("para_p1", null); // Front or back matter text paragraph, level 1 (if multiple levels).
    m_SimpleTags.put("para_p2", null); // Front or back matter text paragraph, level 2 (if multiple levels).
    m_SimpleTags.put("para_pb", null); // Page Break used for new reader portions and children's bibles where content is controlled by the page.
    m_SimpleTags.put("para_periph", null); // Periheral content division marker which should be followed by an additional division argument/title.
    m_SimpleTags.put("para_pm", null); // Embedded text paragraph.
    m_SimpleTags.put("para_pmc", null); // Embedded text closing.
    m_SimpleTags.put("para_pmo", null); // Embedded text opening.
    m_SimpleTags.put("para_pmr", null); // Embedded text refrain (e.g. Then all the people shall say, "Amen!").
    m_SimpleTags.put("para_qa", null); // Poetry text, Acrostic marker/heading.
    m_SimpleTags.put("para_qc", null); // Poetry text, centered.
    m_SimpleTags.put("para_tr", null); // Table row.
    m_SimpleTags.put("para_toc2", null); // Short table of contents text.
    m_SimpleTags.put("para_toc3", null); // Book abbreviation.



    /**************************************************************************/
    /* Tags which we have overtly decided should not appear in OTML. */
    
    m_SimpleTags.put("char_fv", new TagPair(null, null)); // A verse number within the footnote text.
    m_SimpleTags.put("char_rem", new TagPair(null, null)); // Remark.
    m_SimpleTags.put("figure_fig", new TagPair(null, null)); // Figure.
    m_SimpleTags.put("hash_comment", new TagPair(null, null)); // Generated internally by the Xerces parser if it sees an XML comment.
    m_SimpleTags.put("hash_ul", new TagPair(null, null)); // Used to makr the start of a list.
    m_SimpleTags.put("para_b", new TagPair(null, null)); // Poetry text stanza break (e.g. stanza break).
    m_SimpleTags.put("para_h", new TagPair(null, null)); // Running header text for a book.
    m_SimpleTags.put("para_h1", new TagPair(null, null)); // Running header text.
    m_SimpleTags.put("para_h2", new TagPair(null, null)); // Running header text, left side of page.
    m_SimpleTags.put("para_h3", new TagPair(null, null)); // Running header text, right side of page.
    m_SimpleTags.put("para_ide", new TagPair(null, null)); // File encoding information.
    m_SimpleTags.put("para_ie", new TagPair(null, null)); // Introduction ending marker.
    m_SimpleTags.put("para_rem", new TagPair(null, null)); // Remark.
    m_SimpleTags.put("para_restore", new TagPair(null, null)); // Comment about when text was restored.


    
    /**************************************************************************/
    /* Tags which are simply converted to an opening and / or closing tag. */
    
    // For a translational addition to the text.
    m_SimpleTags.put("char_add", new TagPair("<i>", "</i> "));
    
    // A character style, use bold text.
    m_SimpleTags.put("char_bd", new TagPair("<b>", "</b> "));
    
    // A character style, use bold + italic text.
    m_SimpleTags.put("char_bdit", new TagPair("<b><i>", "</i></b> "));
    
    // For the quoted name of a book.
    m_SimpleTags.put("char_bk", new TagPair("<i>", "</i> "));
    
    // Second (alternate) chapter number.
    m_SimpleTags.put("char_ca", new TagPair("(", ")"));
    
    // A character style, use emphasized text style.
    m_SimpleTags.put("char_em", new TagPair("<i>", "</i> "));
    
    // A footnote keyword.
    m_SimpleTags.put("char_fk", new TagPair("<i><b>", "</b></i> "));
    
    // A footnote scripture quote or alternate rendering.
    m_SimpleTags.put("char_fq", new TagPair(" <i>", "</i> "));
    
    // A footnote alternate rendering for a portion of scripture text.
    m_SimpleTags.put("char_fqa", new TagPair("<i>", "</i> "));
    
    // Footnote text, Protocanon.
    m_SimpleTags.put("char_ft", new TagPair("", ""));
    
    // A character style, use italic text.
    m_SimpleTags.put("char_it", new TagPair("<i>", "</i> "));
    
    // For a keyword.
    m_SimpleTags.put("char_k", new TagPair("<b><i>", "</i></b> "));
    
    // Poetry text, Selah.
    m_SimpleTags.put("char_qs", new TagPair("<i>", "</i> "));
    
    // A cross-reference indicating the source text for the preceding quotation.
    m_SimpleTags.put("char_rq", new TagPair("<i>", "</i> "));

    // Character style, for small capitalization text.
    m_SimpleTags.put("char_sc", new TagPair("<sc>", "</sc>"));
    
    // For the signature of the author of an Epistle.
    m_SimpleTags.put("char_sig", new TagPair("<i>", "</i> "));
    
    // For transliterated words.
    m_SimpleTags.put("char_tl", new TagPair(" <i>", "</i> "));
    
    // Second (alternate) verse number (for coding dual numeration in Psalms; see also NRSV Exo 22.1-4).
    m_SimpleTags.put("char_va", new TagPair("(", ")"));
    
    // A wordlist text item.
    m_SimpleTags.put("char_w", new TagPair("<DictionaryEntry Keyword=\"", "\">"));
    
    // For marking the words of Jesus.
    m_SimpleTags.put("char_wj", new TagPair("<j>", "</j>"));
    
    // The cross reference target reference(s), protocanon only.  $$$ Why not the same as ft?
    m_SimpleTags.put("char_xt", new TagPair("<p>", "</p>" + C_LineSep));

    // A Hebrew text heading, to provide description (e.g. Psalms).
    m_SimpleTags.put("para_d", new TagPair(headingBr(2) + "<comment>class=\"d\"</comment><h6 halign=\"center\">", "</h6>"));
    
    // Introduction major title, level 1 (if single level).
    m_SimpleTags.put("para_imt", new TagPair(headingBr(4) +"<comment>class=\"imt1\"</comment><h4 halign=\"center\">", "</h4>"));
    
    // Introduction major title, level 1 (if multiple levels).
    m_SimpleTags.put("para_imt1", new TagPair(headingBr(4) + "<comment>class=\"imt1\"</comment><h4 halign=\"center\">", "</h4>"));
    
    // Introduction major title, level 2.
    m_SimpleTags.put("para_imt2", new TagPair(headingBr(3) + "<comment>class=\"imt2\"</comment><h4 halign=\"center\">", "</h4>"));

    // Introduction major title, level 3.
    m_SimpleTags.put("para_imt3", new TagPair(headingBr(3) + "<comment>class=\"imt3\"</comment><h4 halign=\"center\">", "</h4>"));
    
    // Introduction major title, level 4 (usually within parenthesis).
    m_SimpleTags.put("para_imt4", new TagPair(headingBr(2) + "<comment>class=\"imt4\"</comment><h4 halign=\"center\">", "</h4>"));
    
    // Introduction major title at introduction end, level 1 (if single level).
    m_SimpleTags.put("para_imte", new TagPair(headingBr(4) + "<comment>class=\"imte1\"</comment><h4 halign=\"center\">", "</h4>"));
    
    // Introduction major title at introduction end, level 1 (if multiple levels).
    m_SimpleTags.put("para_imte1", new TagPair(headingBr(4) + "<comment>class=\"imte1\"</comment><h4 halign=\"center\">", "</h4>"));
    
    // Introduction major title at introduction end, level 2.
    m_SimpleTags.put("para_imte2", new TagPair(headingBr(3) + "<comment>class=\"imte2\"</comment><h4 halign=\"center\">", "</h4>"));
    
    // A list entry, level 1 (if single level).
    m_SimpleTags.put("para_li", new TagPair(C_LineSep + "<li>", "</li>"));
    
    // A list entry, level 1 (if multiple levels).
    m_SimpleTags.put("para_li1", new TagPair(C_LineSep + "<li>", "</li>"));
    
    // A list entry, level 2.
    m_SimpleTags.put("para_li2", new TagPair(C_LineSep + "<li>", "</li>"));
    
    // A list entry, level 3.
    m_SimpleTags.put("para_li3", new TagPair(C_LineSep + "<li>", "</li>"));
    
    // A list entry, level 4.
    m_SimpleTags.put("para_li4", new TagPair(C_LineSep + "<li>", "</li>"));
    
    // Paragraph text, with no first line indent (may occur after poetry) aka: Paragraph Continuation.
    m_SimpleTags.put("para_m", new TagPair("<p>", "</p>"));
    
    // Paragraph text, indented, with no first line indent; often used for discourse.
    m_SimpleTags.put("para_mi", new TagPair("<p>", "</p>"));
    
    // A major section division references range heading.
    m_SimpleTags.put("para_mr", new TagPair(headingBr(2) + "<comment>class=\"mr\"</comment><h5 halign=\"center\">", "</h5>"));
    
    // A major section division heading, level 1 (if single level).
    m_SimpleTags.put("para_ms", new TagPair(headingBr(4) + "<comment>class=\"ms1\"</comment><h3 halign=\"center\">", "</h3>"));
    
    // A major section division heading, level 1 (if multiple levels).
    m_SimpleTags.put("para_ms1", new TagPair(headingBr(4) + "<comment>class=\"ms1\"</comment><h3 halign=\"center\">", "</h3>"));
    
    // A major section division heading, level 2.
    m_SimpleTags.put("para_ms2", new TagPair(headingBr(3) + "<comment>class=\"ms2\"</comment><h4 halign=\"center\">", "</h4>"));
    
    // A major section division heading, level 3.
    m_SimpleTags.put("para_ms3", new TagPair(headingBr(2) + "<comment>class=\"ms3\"</comment><h4 halign=\"center\">", "</h4>"));
    
    // The main title of the book (if single level).
    m_SimpleTags.put("para_mt", new TagPair(headingBr(4) + "<comment>class=\"mt1\"</comment><h2 halign=\"center\">", "</h2>"));
    
    // The main title of the book (if multiple levels).
    m_SimpleTags.put("para_mt1", new TagPair(headingBr(4) + "<comment>class=\"mt1\"</comment><h2 halign=\"center\">", "</h2>"));
    
    // A secondary title usually occurring before the main title.
    m_SimpleTags.put("para_mt2", new TagPair(headingBr(3) + "<comment>class=\"mt2\"</comment><h2 halign=\"center\"><i>", "</i></h2>"));
    
    // The main title of the book repeated at the end of the book, level 1 (if single level).
    m_SimpleTags.put("para_mte", new TagPair(headingBr(4) + "<comment>class=\"mte1\"</comment><h2 halign=\"center\">", "</h2>"));
    
    // The main title of the book repeated at the end of the book, level 1 (if multiple levels).
    m_SimpleTags.put("para_mte1", new TagPair(headingBr(4) + "<comment>class=\"mte1\"</comment><h2 halign=\"center\">", "</h2>"));
    
    // A secondary title occurring before or after the 'ending' main title.
    m_SimpleTags.put("para_mte2", new TagPair(headingBr(3) + "<comment>class=\"mte2\"</comment><h4 halign=\"center\">", "</h4>"));
    
    // Paragraph text, with no break from previous paragraph text (at chapter boundary).
    m_SimpleTags.put("para_nb", new TagPair("<p>", "</p>"));
    
    // Paragraph text, with first line indent.
    m_SimpleTags.put("para_p", new TagPair(C_LineSep + "<p>", "</p>"));
    
    // Paragraph text, centered (for Inscription).
    m_SimpleTags.put("para_pc", new TagPair("<p><c>", "</c></p>"));
    
    // Paragraph text, level 1 indent (if single level), with first line indent; often used for discourse.
    m_SimpleTags.put("para_pi", new TagPair("<p>", "</p>"));
    
    // Paragraph text, level 1 indent (if multiple levels), with first line indent; often used for discourse.
    m_SimpleTags.put("para_pi1", new TagPair("<p>", "</p>"));
    
    // Paragraph text, level 2 indent, with first line indent; often used for discourse.
    m_SimpleTags.put("para_pi2", new TagPair("<p>", "</p>"));
    
    // Paragraph text, level 3 indent, with first line indent; often used for discourse.
    m_SimpleTags.put("para_pi3", new TagPair("<p>", "</p>"));
    
    // Poetry text, level 1 indent (if single level).
    m_SimpleTags.put("para_q", new TagPair(C_LineSep + "<poetry level=1>", "</poetry>"));
    
    // Poetry text, level 1 indent (if multiple levels).
    m_SimpleTags.put("para_q1", new TagPair(C_LineSep + "<poetry level=1>", "</poetry>"));
    
    // Poetry text, level 2 indent.
    m_SimpleTags.put("para_q2", new TagPair(C_LineSep + "<poetry level=2>", "</poetry>"));
    
    // Poetry text, level 3 indent.
    m_SimpleTags.put("para_q3", new TagPair(C_LineSep + "<poetry level=3>", "</poetry>"));
    
    // Poetry text, level 4 indent.
    m_SimpleTags.put("para_q4", new TagPair(C_LineSep + "<poetry level=3>", "</poetry>"));
    
    // Poetry text, Right Aligned.
    m_SimpleTags.put("para_qr", new TagPair("<i>", "</i>"));

    // Parallel reference(s).
    m_SimpleTags.put("para_r", new TagPair("<comment>class=\"r\"</comment><h5 halign=\"center\">", "</h5>"));
    
    // A section heading, level 1 (if single level).
    m_SimpleTags.put("para_s", new TagPair(headingBr(4) + "<comment>class=\"s1\"</comment><h4 halign=\"center\"><i>", "</i></h4>"));
    
    // A section heading, level 1 (if multiple levels).
    m_SimpleTags.put("para_s1", new TagPair(headingBr(4) + "<comment>class=\"s1\"</comment><h4 halign=\"center\"><i>", "</i></h4>"));
    
    // Variant_Section_Head - a section heading, level 2 (e.g. Proverbs 22-24)
    m_SimpleTags.put("para_s2", new TagPair(headingBr(3) + "<comment>class=\"s2\"</comment><h4 halign=\"center\"><i>", "</i></h4>"));
    
    // A section heading, level 3 (e.g. Genesis "The First Day").
    m_SimpleTags.put("para_s3", new TagPair(headingBr(3) + "<comment>class=\"s3\"</comment><h4 halign=\"center\"><i>", "</i></h4>"));
    
    // A section heading, level 4.
    m_SimpleTags.put("para_s4", new TagPair(headingBr(2) + "<comment>class=\"s4\"</comment><h4 halign=\"center\"><i>", "</i></h4>"));
    
    // A heading, to identify the speaker (e.g. Job).
    m_SimpleTags.put("para_sp", new TagPair("<br /><i>", "</i><p />"));
    
     // A section division references range heading.
    m_SimpleTags.put("para_sr", new TagPair("<comment>class=\"sr\"</comment><h5 halign=\"center\">", "</h5>"));

    // Long table of contents text.
    m_SimpleTags.put("para_toc1", new TagPair("<toc level=1 LongName=\"", "\"></toc>"));
    
    // USX root.
     m_SimpleTags.put("usx", new TagPair("", ""));
 }

  
  /****************************************************************************/
  /* When processing scripture files, we have to work on the assumption that
   * there will be a publisher notes file, and therefore include a reference
   * to it.  However, in some cases we may not actually have such a file,
   * in which case we have to remove the reference to the publisher notes
   * from the scripture file.
   */
  
  private void removeReferenceToPublisherNotesIfNecessary (String folderPath, String fileName) throws IOException
  {
    /*------------------------------------------------------------------------*/
    String extension = Config.instance().getOutputFileExtension();
    String notesFileName = fileName.substring(0, fileName.length() - extension.length() - 1) + "-Footnotes."  + extension;
    if (new File(folderPath + File.separator + notesFileName).exists()) return;
 
    
    
    /*------------------------------------------------------------------------*/
    String encoding = Config.instance().getEncoding();
    String inputFileName = folderPath + File.separator + fileName;
    FileInputStream fis = new FileInputStream(inputFileName);
    UnicodeReader unicodeReader = new UnicodeReader(fis, encoding);
    BufferedReader rdr = new BufferedReader(unicodeReader);

    String outputFileName = folderPath + File.separator + "$$Jamie$$";
    FileOutputStream fos = new FileOutputStream(outputFileName);
    OutputStreamWriter osWriter = new OutputStreamWriter(fos, encoding);
    BufferedWriter writer = new BufferedWriter(osWriter);
    
    Pattern pat = Pattern.compile("\\<PublisherNotesFile.*");



    /*------------------------------------------------------------------------*/
    String s = null;
    while ((s = rdr.readLine()) != null)
      if (!pat.matcher(s).matches())
      {
        writer.write(s);
        writer.newLine();
      }

    
    
    /*------------------------------------------------------------------------*/
    rdr.close();
    unicodeReader.close();
    fis.close();
    
    writer.close();
    osWriter.close();
    fos.close();
    
 
    
    /*------------------------------------------------------------------------*/
    File inputFile = (new File(inputFileName));
    inputFile.delete();
    (new File(outputFileName)).renameTo(inputFile);
  }
  
  
  /****************************************************************************/
  /* Basically, this holds the opening and closing tags for "simple" tags.
   * The opening tag is output when you hit the opening USX tag, and the
   * closing tag is output when you hit the closing USX tag.
   */
  
  private class TagPair
  {
    private TagPair (String openingTag, String closingTag) { m_OpeningTag = openingTag; m_ClosingTag = closingTag; }
    private String getOpeningTag () { return m_OpeningTag; }
    private String getClosingTag () { return m_ClosingTag; }
    private String m_OpeningTag = null;
    private String m_ClosingTag = null;
  }
  
  private HashMap<String, TagPair> m_SimpleTags = new HashMap<String, TagPair>();





  /****************************************************************************/
  /* Used to suppress processing of certain USFM tags in certain circumstances.
     For example, OliveTree can't cope with us generating <br /> in the middle
     of headings, so anything which generates a <br /> needs to be suppressed.
   
     I assume here that style is unique, in the sense that you will never have
     the same style used on two different kinds of tags.  (I do this merely
     to make it slightly easier to initialise the tables -- it would be easy
     to do better if necessary.)
   
     m_SuppressorsOf is therefore keyed on style alone, not on tagName plus
     style.  The entry for a given style contains a "."-separated list of all
     the tagName-plus-style combinations which suppress tags containing that
     style.
   
     This information is fine for checking whether suppressions are needed,
     because UsxParser maintains a string-based stack which says which
     tagName-plus-style combinations are currently active.
   
     I also provide a routine here which indicates whether a given tagName-plus-
     style combination should suppress a tag with a given style.  At the time
     of writing, I use this purely with the pseudo tag "//".  This never
     actually shows up as a tag in its own right (it always appears within
     text), but nonetheless it needs to be suppressed in certain circumstances
     and it is convenient to use the present structure to tell us when.
 */

  private final ContextualSuppressions m_ContextualSuppressions = new ContextualSuppressions();
  private class ContextualSuppressions
  {
    /**************************************************************************/
    private HashMap<String, String> m_SuppressorsOf = new HashMap<String, String>(30);


    /**************************************************************************/
    private ContextualSuppressions ()
    {
      suppressBWithinA(UsxParser.makeKey("para", "h"  ), "//.io.io1.io2.io3.io4.m");
      suppressBWithinA(UsxParser.makeKey("para", "r"  ), "//.io.io1.io2.io3.io4.m");
      suppressBWithinA(UsxParser.makeKey("para", "s"  ), "//.io.io1.io2.io3.io4.m");
      suppressBWithinA(UsxParser.makeKey("para", "iot"), "//.io.io1.io2.io3.io4.m");
      suppressBWithinA(UsxParser.makeKey("para", "is" ), "//.io.io1.io2.io3.io4.m");
      suppressBWithinA(UsxParser.makeKey("para", "q"  ), "//.p");
    }

    
    /**************************************************************************/
    /* "a" is a tag within which we need to needToSupress certain tags.  "b" is a
       list of the tags which need to be suppressed. */

    private void suppressBWithinA (String a, String b)
    {
      String tags[] = b.split("\\.");
      for (int i = 0; i < tags.length; ++i)
      {
        if (m_SuppressorsOf.containsKey(tags[i]))
          m_SuppressorsOf.put(tags[i], m_SuppressorsOf.get(tags[i]) + a + ".");
	else
          m_SuppressorsOf.put(tags[i], a + ".");
      }
    }


    /**************************************************************************/
    /* Returns an indication of whether, given the current state of the input,
     * we have reason to needToSupress output of the current tag.
     */
    
    boolean needToSuppress (String tagName, String style)
    {
      if (!m_SuppressorsOf.containsKey(style)) return false;
      
      String s = m_SuppressorsOf.get(style);
      String tags[] = s.split("\\.");
      for (int i = 0; i < tags.length; ++i)
        if (m_Parser.tagStackWithoutLevelsContains(tags[i])) return true;
      return false;
    }
    
    
    /**************************************************************************/
    /* Returns an indication of whether the tagName / style combination are
     * recorded as suppressing "what".
     */
    
    boolean suppresses (String tagName, String style, String what)
    {
      if (!m_SuppressorsOf.containsKey(what)) return false;
      String s = m_SuppressorsOf.get(what);
      return -1 != s.indexOf("." + UsxParser.makeKey(tagName, style) + ".");
    }
  }





  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                    Specific tag and text handlers                      **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  /* Book. */
  
  @Override protected void start_book_id (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    commonStartOfBook(eltDetails);
    m_OtmlBookNo = BibleDetails.instance().getParameterForBook_E(Bookmark.instance().getFullUbsName(), "otmlBookNumber");
    writeToCurrentDest(""); // Force the file header to be written.
    if (!OutputManager.instance().getMemoryBuffering(OutputManager.C_Destination_Scripture))
      OutputManager.instance().setMemoryBuffering(OutputManager.C_Destination_Scripture, true); // Buffer until we're in a verse.
  }
  
  
  @Override protected void end_book_id (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
  }



  /****************************************************************************/
  /* Chapter. */
  
  @Override protected void start_chapter_c (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    /**************************************************************************/
    commonStartOfChapter(eltDetails);
    int chapterNo = Bookmark.instance().getChapter();



    /**************************************************************************/
    /* Buffer until we are in a verse. */

    if (!OutputManager.instance().getMemoryBuffering(OutputManager.C_Destination_Scripture))
      OutputManager.instance().setMemoryBuffering(OutputManager.C_Destination_Scripture, true);


   
    /**************************************************************************/
    /* In the original version of the OliveTree processing, we had to output a
     * mixture of stuff needed for OliveTree and stuff for the proofer to get
     * its teeth into.
     * 
     * We no longer need to do anything special to satisfy the needs of
     * OliveTree, because OliveTree now does internally everything which
     * we were forcing it to do.
     * 
     * We may, in theory, still need stuff for the proofer, although I suspect
     * this will be relatively short-lived -- it looks as though OliveTree's
     * own reader software will shortly do everything which the proofer was
     * doing previously.
     */
      
     if (m_IncludeHooksForProofer)
         writeToCurrentDest("<comment>OutputChapterNumberAsDropCap " +
  			    NumeralProcessor.instance().toVernacular(chapterNo) +
  			    "</comment>");
  }
  
  @Override protected void end_chapter_c (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
  }


  /****************************************************************************/
  /* Footnote origin. */
  
  @Override protected void start_char_fr (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_start_char_fr_xo(tagName, style, level, eltDetails, destination);
  }

  @Override protected void end_char_fr (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_end_char_fr_xo(tagName, style, level, eltDetails, destination);
  }


  /****************************************************************************/
  /* Cross-reference origin. */
  
  @Override protected void start_char_xo (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_start_char_fr_xo(tagName, style, level, eltDetails, destination);
  }

  @Override protected void end_char_xo (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_end_char_fr_xo(tagName, style, level, eltDetails, destination);
  }


  /****************************************************************************/
  @Override protected void start_extension_introductionBlock
    (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    writeToCurrentDest("<comment>introStart</comment><v" + m_OtmlBookNo + ".220.0>");
  }
  
  /****************************************************************************/
  @Override protected void end_extension_introductionBlock
    (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    writeToGivenDest(OutputManager.C_Destination_Introduction, "<comment>introEnd</comment>");
  }
  
  
  /****************************************************************************/
  /* Footnote. */
  
  @Override protected void start_note_f (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_start_note_f_x(tagName, style, level, eltDetails, destination);
  }

  @Override protected void end_note_f (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_end_note_f_x(tagName, style, level, eltDetails, destination);
  }


  /****************************************************************************/
  /* Cross-reference. */
  
  @Override protected void start_note_x (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_start_note_f_x(tagName, style, level, eltDetails, destination);
  }

  @Override protected void end_note_x (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_end_note_f_x(tagName, style, level, eltDetails, destination);
  }


  /****************************************************************************/
  /* Introduction outline text. */
  
  @Override protected void start_para_io (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_start_para_io_iot_ip_is(tagName, style, level, eltDetails, destination);
  }

  @Override protected void end_para_io (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_end_para_io_iot_ip_is(tagName, style, level, eltDetails, destination);
  }


  @Override protected void start_para_io1 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_start_para_io_iot_ip_is(tagName, style, level, eltDetails, destination);
  }

  @Override protected void end_para_io1 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_end_para_io_iot_ip_is(tagName, style, level, eltDetails, destination);
  }


  @Override protected void start_para_io2 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_start_para_io_iot_ip_is(tagName, style, level, eltDetails, destination);
  }

  @Override protected void end_para_io2 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_end_para_io_iot_ip_is(tagName, style, level, eltDetails, destination);
  }


  @Override protected void start_para_io3 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_start_para_io_iot_ip_is(tagName, style, level, eltDetails, destination);
  }

  @Override protected void end_para_io3 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_end_para_io_iot_ip_is(tagName, style, level, eltDetails, destination);
  }


  @Override protected void start_para_io4 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_start_para_io_iot_ip_is(tagName, style, level, eltDetails, destination);
  }

  @Override protected void end_para_io4 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_end_para_io_iot_ip_is(tagName, style, level, eltDetails, destination);
  }



  /****************************************************************************/
  /* Introduction outline title. */
  
  @Override protected void start_para_iot (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_start_para_io_iot_ip_is(tagName, style, level, eltDetails, destination);
  }

  @Override protected void end_para_iot (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_end_para_io_iot_ip_is(tagName, style, level, eltDetails, destination);
  }



  /****************************************************************************/
  /* Introduction prose paragraph. */
  
  @Override protected void start_para_ip (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_start_para_io_iot_ip_is(tagName, style, level, eltDetails, destination);
  }

  @Override protected void end_para_ip (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_end_para_io_iot_ip_is(tagName, style, level, eltDetails, destination);
  }


  /****************************************************************************/
  /* Introduction section heading. */

  @Override protected void start_para_is (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_start_para_io_iot_ip_is(tagName, style, level, eltDetails, destination);
  }

  @Override protected void end_para_is (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_end_para_io_iot_ip_is(tagName, style, level, eltDetails, destination);
  }


  @Override protected void start_para_is1 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_start_para_io_iot_ip_is(tagName, style, level, eltDetails, destination);
  }

  @Override protected void end_para_is1 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_end_para_io_iot_ip_is(tagName, style, level, eltDetails, destination);
  }


  @Override protected void start_para_is2 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_start_para_io_iot_ip_is(tagName, style, level, eltDetails, destination);
  }

  @Override protected void end_para_is2 (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    common_end_para_io_iot_ip_is(tagName, style, level, eltDetails, destination);
  }


  /****************************************************************************/
  /* Poetry text, embedded.  Slightly fiddly, this one ...

     We don't want any trailing <br /> or <p /> to begin with.

     We then need to output <br /> if we have text, and <p /> if we don't.

     Trouble is, we don't know yet whether we have any text.  So I write
     out the <br /> in the start tag processing on the assumption that we will.

     In the end tag processing, I check if we did have any text.  If we did
     not, I get rid of the <br /> and output <p /> instead. */
  
  @Override protected void start_para_qm (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    while (OutputManager.instance().removeTrailingStringFromBufferOnce("<br />") ||
	   OutputManager.instance().removeTrailingStringFromBufferOnce("<p />"))
      {} // Do nothing.

    writeToCurrentDest("<br />");
  }
  
  @Override protected void end_para_qm (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    if (0 == m_LengthOfMostRecentHashText)
    {
      OutputManager.instance().removeTrailingStringFromBufferOnce("<br />");
      writeToCurrentDest("<p />");
    }
  }


  /****************************************************************************/
  /* Start / end of a list. */
  
  /*
  @Override protected void start_userDefinedPara_bra_ul (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
     writeToGivenDest(destination, "<ul>");
  }

  @Override protected void start_userDefinedPara_ket_ul (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
     writeToGivenDest(destination, "</ul>");
  }
   */

  
  /****************************************************************************/
  /* Verse. */
  
  @Override protected void start_verse_v (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    /**************************************************************************/
    commonStartOfVerse(eltDetails);
    int chapterNo = Bookmark.instance().getChapter();
    int verseLow = Bookmark.instance().getVerseLow();
    int verseHigh = Bookmark.instance().getVerseHigh();
    boolean startOfChapter = (1 == verseLow);
    boolean startOfBook = startOfChapter && (1 == chapterNo);
    
    int firstVerseForWhichToOutputAnchor = Bookmark.instance().getVerseLow();
    if (Bookmark.instance().getVerseLowEqualsPreviousVerseHigh()) ++firstVerseForWhichToOutputAnchor;



    /**************************************************************************/
    OutputManager.instance().setMemoryBuffering(destination, false);
    
    

    /**************************************************************************/
    /* Pull out any buffered introductory material for use shortly. */
    
    String intro = OutputManager.instance().extractMemoryBufferContents("(<comment>introStart</comment>.+<comment>introEnd</comment>)");
    if (0 != intro.length())
    {
      intro = intro.substring("<comment>introStart</comment>".length());
      intro = intro.substring(0, intro.length() - "<comment>introEnd</comment>".length());
    }
    
    

    /**************************************************************************/
    /* According to Bob's scheme, at the start of a book, we want ...
     * 
     *    The TOC
     *    hr
     *    The main book title
     *    Any introductory material
     * 
     * We know we're at the start of a book if we've just had a chapter tag,
     * and if the current chater is 1.
     */
    
    if (startOfBook)
    {
      /*--------------------------------------------------*/
      /* Obtain any TOC buffered for this file.  This will
       * actually pull out _all_ such tags, one at a time,
       * although I think I'm kinda assuming there's only
       * one of them. */
    
      while (true)
      {
        String s = OutputManager.instance().extractMemoryBufferContents("(<toc.+?<\\/toc>)"); // Extract any TOC information from the buffer.
        if (0 == s.length()) break;
        writeToCurrentDest(s);
      }

      writeToCurrentDest("<hr />");
   

      
      /*--------------------------------------------------*/
      /* Extract and write out header information.  Bob
       * assumes that the first header will be the main
       * book title.  I'm not so sure we can rely on the
       * translators to work that way, but I can't think
       * of anything better.
       */
      
      String s = OutputManager.instance().extractMemoryBufferContents("((<headingBr />)?(<comment>[^>]+</comment>)?<h(\\d*).*?</h\\4>)");
      if (0 != s.length()) writeToCurrentDest(s);
      
      
      
      /*--------------------------------------------------*/
      /* Output any intro, followed by hr to set it off
       * from the text.
       */
      
      writeToCurrentDest(intro);
      if (0 != intro.length()) { writeLineToCurrentDest(""); writeToCurrentDest("<hr />"); }
    } // startOfBook
      

    
    /**************************************************************************/
    /* According to Bob's scheme, at the start of a chapter, we want ...
     * 
     *    CRLF
     *    hr
     *    <v##.cc.vv> (which I believe needs to be repeated if this covers
     *       multiple verses)
     *    CRLF
     * 
     * I prefix this below with any remaining buffered text.  Bob doesn't make
     * allowance for this, and I don't know whether there can actually be any,
     * and if there is I don't really know what we want to generate for it,
     * but if there is some, I'm loathe simply to throw it away.
     */

    if (startOfChapter)
    {
      if (!startOfBook) { writeLineToCurrentDest(""); writeToCurrentDest("<hr />"); }

        
        
      /*--------------------------------------------------*/
      /* Verse marker. */
   
      for (int i = firstVerseForWhichToOutputAnchor; i <= Bookmark.instance().getVerseHigh(); ++i)
        writeLineToCurrentDest("<v" + m_OtmlBookNo + "." + Integer.toString(chapterNo) + "." + Integer.toString(i) + ">");
      
      
      
      /*--------------------------------------------------*/
      /* My bits.  Extract header information first, and
       * then anything else.
       */
      
      while (true)
      {
        String s = OutputManager.instance().extractMemoryBufferContents("((<headingBr />)?(<comment>[^>]+</comment>)?<h(\\d*).*?</h\\4>)");
        if (0 == s.length()) break;
        writeToCurrentDest(s);
      }

      String s = OutputManager.instance().getMemoryBufferContents();
      OutputManager.instance().clearMemoryBufferContents();
      if (0 != s.trim().length()) writeLineToCurrentDest(s);
    } // startOfChapter




    /**************************************************************************/
    if (!startOfChapter)
      for (int i = firstVerseForWhichToOutputAnchor; i <= Bookmark.instance().getVerseHigh(); ++i)
      {
        writeToCurrentDest("<v");
        writeToCurrentDest(m_OtmlBookNo);
        writeToCurrentDest(".");
        writeToCurrentDest(Integer.toString(chapterNo));
        writeToCurrentDest(".");
        writeToCurrentDest(Integer.toString(i));
        writeLineToCurrentDest(">");
      }
      
      
      
    /**************************************************************************/
    /* The actual verse number(s). */

    writeToCurrentDest("<VerseNumber>");
    writeToCurrentDest(NumeralProcessor.instance().toVernacular(verseLow));
    if (verseLow != verseHigh) { writeToCurrentDest("<sup>-</sup>"); writeToCurrentDest(NumeralProcessor.instance().toVernacular(verseHigh)); }
    writeToCurrentDest("</VerseNumber>");
  }

  @Override protected void end_verse_v (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
  }


  /****************************************************************************/
  /* Common processing for fr, xo. Basically if it's of the form
     bookname chapter:verse then I output it as it is.  If there's no
     bookname at the front, then I use the current vernacular name. */

  private void common_start_char_fr_xo (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    /**************************************************************************/
    writeLineToCurrentDest("");
    writeToCurrentDest("<hr /><v");
    writeToCurrentDest(Integer.toString(Bookmark.instance().getBookNo())); writeToCurrentDest(".");
    writeToCurrentDest(Integer.toString(Bookmark.instance().getChapter())); writeToCurrentDest(".");
    writeToCurrentDest(Integer.toString(0 == Bookmark.instance().getVerseLow() ? 1 : Bookmark.instance().getVerseLow())); writeToCurrentDest(">");
    //$$$ if (m_IncludeHooksForProofer) writeToCurrentDest("<comment>class=\"name\"</comment>");
    //$$$ writeToCurrentDest("<a name=\"");
    //$$$ writeToCurrentDest(Integer.toString(Bookmark.instance().getBookNo())); writeToCurrentDest(".");
    //$$$ writeToCurrentDest(Integer.toString(Bookmark.instance().getChapter())); writeToCurrentDest(".");
    //$$$ writeToCurrentDest(Integer.toString(Bookmark.instance().getVerseLow())); writeToCurrentDest(".");
    //$$$ writeToCurrentDest(Integer.toString(m_AnchorNo));
    //$$$ writeToCurrentDest("\"></a>");
  }
  
  private void common_end_char_fr_xo (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
  }


  /****************************************************************************/
  /* Common processing for f, x. */
  
  private void common_start_note_f_x (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    /**************************************************************************/
    writeToGivenDest(OutputManager.C_Destination_Scripture, "<xf anchor=\"");
    writeToGivenDest(OutputManager.C_Destination_Scripture, Integer.toString(Bookmark.instance().getBookNo())); writeToGivenDest(OutputManager.C_Destination_Scripture, ".");
    writeToGivenDest(OutputManager.C_Destination_Scripture, Integer.toString(Bookmark.instance().getChapter())); writeToGivenDest(OutputManager.C_Destination_Scripture, ".");
    writeToGivenDest(OutputManager.C_Destination_Scripture, Integer.toString(Bookmark.instance().getVerseLow())); writeToGivenDest(OutputManager.C_Destination_Scripture, ".");
    writeToGivenDest(OutputManager.C_Destination_Scripture, Integer.toString(++m_AnchorNo));
    writeToGivenDest(OutputManager.C_Destination_Scripture, "\" type=pubNote docId=pubNoteDocId>");
    writeToGivenDest(OutputManager.C_Destination_Scripture, m_FootnoteMarkerManager.getFootnoteMarker(eltDetails.getAttribute("caller")));
    if (okToWriteSpace()) writeToGivenDest(OutputManager.C_Destination_Scripture, m_Space);
  }

  private void common_end_note_f_x (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    writeToGivenDest(OutputManager.C_Destination_Scripture, "</xf>");
  }


  /****************************************************************************/
  /* Common processing for io, iot, ip, is. */
  
  private void common_start_para_io_iot_ip_is (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    if ("is".equals(style))
    {
      String tag = 2 == level ? "h5": "h4";
      if (m_IncludeHooksForProofer) writeToCurrentDest("<comment>class=\"is\"</comment>");
      writeToCurrentDest("<" + tag + " halign=\"center\">");
    }
    else
    {
      writeLineToCurrentDest("");
      if (m_IncludeHooksForProofer) writeToCurrentDest("<comment>class=\"" + style + (0 == level ? "" : Integer.toString(level)) + "\"</comment>");
      writeToCurrentDest("<p>");
    }
  }

  private void common_end_para_io_iot_ip_is (String tagName, String style, int level, UsxParser.ElementDetails eltDetails, int destination) throws Exception
  {
    if ("is".equals(style))
    {
      String tag = 2 == level ? "h5": "h4";
      writeToCurrentDest("</" + tag + ">");
    }
    else
      writeToCurrentDest("</p>");
  }

  
  


  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                          File output handling                          **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  /* Used to handle scripture output. */

  private HeaderAndTrailerWriterScripture m_HeaderAndTrailerWriterScripture = new HeaderAndTrailerWriterScripture();
  class HeaderAndTrailerWriterScripture implements OutputStreamHeaderAndTrailerWriter
  {
    @Override public void writeHeader (MyOutputStream stream) throws IOException
    {
      String userCategory = "Bible";

      stream.write("<Title>");
      stream.write(Config.instance().getDcTitle());
      stream.write("</Title>");
      stream.newLine();

      stream.write("<AbbrTitle>");
      stream.write(Config.instance().getIso());
      stream.write("</AbbrTitle>");
      stream.newLine();

      stream.write("<UserCategory>");
      stream.write(userCategory);
      stream.write("</UserCategory>");
      stream.newLine();

      String fileName = Bookmark.instance().getBookElement().getOutputFileName(0);
      fileName = fileName.substring(0, fileName.lastIndexOf(".")) + "." + Config.instance().getConverterUltimateTargetFileExtension();
      stream.write("<OutputFileName>");
      stream.write(fileName);
      stream.write("</OutputFileName>");
      stream.newLine();

      stream.write("<Copyright>");
      stream.write(Config.instance().getCopyright());
      stream.write("</Copyright>");
      stream.newLine();

      stream.write("<DatabaseRegistration>");
      stream.write(Config.instance().getOtmlId());
      stream.write("</DatabaseRegistration>");
      stream.newLine();

      if ("RTL".equalsIgnoreCase(Config.instance().getTextDir()))
      {
        stream.write("<PrimaryDisplayFontGroup name=\"ArabicUnicode\">");
        stream.newLine();
      }

      
      
      /************************************************************************/
      /* We have to assume that we're going to have a publisher notes file
         for the time being.  If in fact we don't end up creating one,
         we'll have to strip this directive out of the file later. */
      
      if ("Combined".equalsIgnoreCase(Config.instance().getOutputFootnotesDisposition()))
      {
        fileName = Config.instance().getOutputFootnotesFileName();
        if (0 != fileName.length()) fileName = new File(fileName).getName();
        stream.write("<PublisherNotesFile file=\"");
        stream.write(fileName);
        stream.write("\" docid=");
        stream.write(Integer.toString(65536 + Integer.parseInt(Config.instance().getOtmlId())));
        stream.write(">");
        stream.newLine();
      }

      fileName = Config.instance().getOtmlCoverFileName();
      if (0 != fileName.length())
      {
        stream.write("<CoverImage>" + fileName + "</CoverImage>");
        stream.newLine();
      }

      String s = Config.instance().getRevision();
      stream.write("<DocumentVersion>");
      stream.write(0 == s.length() ? "1" : s);
      stream.write("</DocumentVersion>");
      stream.newLine();

      stream.write("<BR40_OTML>");
      stream.newLine();

      stream.write("<comment>");
      stream.write(m_RunDateAndTime);
      stream.write("</comment>");
      stream.newLine();
      
      insertPeripheralFile(stream, "*INTRO*.otml");
      insertPeripheralFile(stream, "*PREF*.otml");
    }

    @Override public void writeTrailer (MyOutputStream stream) throws IOException
    {
      insertPeripheralFile(stream, "*END*.otml");
    }
  }


  /****************************************************************************/
  /* Used to handle notes output etc. */

  private HeaderAndTrailerWriterNotes m_HeaderAndTrailerWriterNotes = new HeaderAndTrailerWriterNotes();
  class HeaderAndTrailerWriterNotes implements OutputStreamHeaderAndTrailerWriter
  {
    @Override public void writeHeader (MyOutputStream stream) throws IOException
    {
      stream.write("<Title>");
      stream.write(Config.instance().getDcTitle());
      stream.write("</Title>");
      stream.newLine();

      stream.write("<Copyright>");
      stream.write(Config.instance().getCopyright());
      stream.write("</Copyright>");
      stream.newLine();

      stream.write("<comment>");
      stream.write(m_RunDateAndTime);
      stream.write("</comment>");
      stream.newLine();

      if ("RTL".equalsIgnoreCase(Config.instance().getTextDir()))
      {
        stream.write("<PrimaryDisplayFontGroup name=\"ArabicUnicode\">");
        stream.newLine();
      }
    }

    @Override public void writeTrailer (MyOutputStream stream) throws IOException
    {
    }
  }





  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                                Private                                 **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  /* Copy graphics files from Peripheals to Output (if there are any. */
  
  private void copyGraphics () throws IOException
  {
    String folderNameFrom = Config.instance().getOutputFolderPath() + "../Peripherals";
    String folderNameTo = Config.instance().getOutputFolderPath();
    
    String[] pats = {".*\\.[Pp][Nn][Gg]$", ".*\\.[Jj][Pp][Gg]$", ".*\\.[Jj][Pp][Ee][Gg]$"};
    for (String pat : pats)
    {
      String fileList = Utils.getAllMatchingFilesFromFolder(folderNameFrom, pat, true, ";");
      if (0 != fileList.length())
      {
        String[] files = fileList.split("\\;");
        for (int i = 0; i < files.length; ++i)
          Utils.fileCopy(folderNameTo + File.separator + files[i], folderNameFrom + File.separator + files[i]);
      }
    }
  }
  
  
  /****************************************************************************/
  /* Generates n headingBr tags -- or not.  Originally I had these hardcoded,
   * but Bob felt that blank lines were unnecessary before headings.  I was
   * a bit loathe to remove reference to the blanks altogether in case we ever
   * needed to reinstate them, because that was likely to be a bit of a pain.
   * So rather than hard code them, I left them in as calls to this routine,
   * which I can tweak as necessary.
   */
  
  private String headingBr (int n)
  {
    return "<headingBr />"; // Ignore n, but always give one br, because
                            // a bug in the OTML reader means centered headings
                            // also impose their centering on the previous para.
  }
  
  
  /****************************************************************************/
  /* Insert the PUB file (if there is one). */
  
  private void insertPeripheralFile (MyOutputStream stream, String filePat) throws IOException
  {
    String folderName = Config.instance().getOutputFolderPath() + "../Peripherals";
    String filePath = Utils.getSingleMatchingFileFromFolder(folderName, filePat, false);
    if (0 != filePath.length()) Utils.copyFileToStream(stream, filePath, Config.instance().getEncoding());
  }
  
  
  /****************************************************************************/
  /* Check if it's ok to write a space.  I assume it's not if the current
     output already ends with one, or if it ends with a newline marker,
     or if it ends with <br />, <hr /> or </p>, all of which have the
     effect of a new line in the output.  Note that I assume output is
     going to the present destination. */

  private boolean okToWriteSpace ()
  {
    String peekedData = OutputManager.instance().peekEndOfData(10);
    if (0 == peekedData.length()) return true;
    if (peekedData.endsWith(MyOutputStream.C_PseudoLineSeparatorAsString)) return false;
    if (peekedData.endsWith("<br />")) return false;
    if (peekedData.endsWith("<hr />")) return false;
    if (peekedData.endsWith("</p>"))   return false;
    if (peekedData.matches("\\p{Z}$")) return false;
    return true;
  }
  
  
  /****************************************************************************/
  /** Writes out text to the current destination. */

  @Override protected void writeToCurrentDest (String s) throws IOException
  {
    writeToGivenDest(OutputManager.instance().peekDestination(), s);
  }

  
 /*****************************************************************************/
 /* Write out text to a given destination, but don't change the
    destination stack. */   

  @Override protected void writeToGivenDest (int destination, String s) throws IOException
  {
    /**************************************************************************/
    /* If we're buffering, simply output the text as-is -- we'll sort out the
     * correct processing when we cease buffering and start using the text.
     */
    
    if (OutputManager.instance().getMemoryBuffering(destination))
    {
      super.writeToGivenDest(destination, s);
      return;
    }
    

    
    /**************************************************************************/
    if (!m_IncludeHooksForProofer) s = s.replaceAll("<comment>[^>]+</comment>", "");


    
    /**************************************************************************/
    String chunks[] = s.split("\\<");
    for (int i = 0; i < chunks.length; ++i)
    {
      if (0 == chunks[i].length())
        ;
      else if (chunks[i].startsWith("/p>"))
      {
        m_LastTag = "/p";  
        super.writeToGivenDest(destination, "<" + chunks[i]); 
      }
      else if (chunks[i].startsWith("/comment>"))
      {
        super.writeToGivenDest(destination, "<" + chunks[i]); 
      }
      else if (chunks[i].startsWith("hr />"))  
      { 
        m_LastTag = "/hr"; 
        super.writeToGivenDest(destination, "<" + chunks[i]); 
      }
      else if (chunks[i].matches("^/h\\d\\>")) 
      { 
        m_LastTag = "/hd"; 
        super.writeToGivenDest(destination, "<" + chunks[i]); 
      }
      else if (chunks[i].startsWith("br />"))
      { 
        if (-1 == "/hr/br/v".indexOf(m_LastTag)) super.writeToGivenDest(destination, "<" + chunks[i]);
        m_LastTag = "/br"; 
      }
      else if (chunks[i].startsWith("headingBr />"))
      { 
        if (-1 == "/hd/hr/br/v".indexOf(m_LastTag))
          super.writeToGivenDest(destination, "<br />" + chunks[i].substring(12));
        m_LastTag = "/br";
      }
      else if (chunks[i].matches(MyOutputStream.C_PseudoLineSeparatorAsPat + "*"))
      {
        super.writeToGivenDest(destination, chunks[i]); 
      }
      else if (chunks[i].matches("v\\d+.*"))
      {
        m_LastTag = "/v";  
        super.writeToGivenDest(destination, "<" + chunks[i]); 
      }
      else
      {
        super.writeToGivenDest(destination, (0 == i ? "" : "<") + chunks[i]);
        m_LastTag = "xx";
      }
    }
  }



 
  



  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                          Specific tag handlers                         **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  /* Stringifier to render scripture references in xb form. */
  
  private class RefsAsXbOutputter extends ParsedTextOutputter
  {
    /**************************************************************************/
    @Override public String toString (ParsedTextElementBibleRef ref,
                                      BibleReferenceSupport.Length fullOrAbbreviated,
                                      BibleReferenceSupport.Language englishOrVernacular,
                                      int sepStyle,
                                      boolean includeDefaultedValues)
    {
      boolean followedByRangeSeparator = (-1 != BibleReferenceSupport.instance().isRangeSeparator(ref.getNextSeparatorCode()));
      boolean precededByRangeSeparator = (-1 != BibleReferenceSupport.instance().isRangeSeparator(ref.getPreviousSeparatorCode()));
      
      boolean targetExists =
        BookAssembly.instance().
          referenceTargetExists(BibleDetails.instance().getAbbreviatedUbsName_E(ref.getFullEnglishBookName()),
                                                                                ref.getChapterAsInteger(NumeralProcessor.instance()));
      
      boolean wantRef = Config.instance().getGenerateLinksForAllScriptureRefs();
      wantRef = wantRef && targetExists;
      
      String s = "";
      
      if (wantRef && !precededByRangeSeparator)
        s = "<xb=" +
            BibleDetails.instance().getParameterForBook_E(ref.getFullEnglishBookName(), "otmlBookNumber") +
            "." +
            ref.getChapter() +
	    "." +
	    ref.getVerse() + ref.getSubverse() + 
            ">";
      
      s += ref.toString(fullOrAbbreviated, englishOrVernacular, sepStyle, includeDefaultedValues);
      
      if (wantRef && !followedByRangeSeparator) s += "</xb>";
      
      return s;
    }
  }
 
  
  /****************************************************************************/
  /* Used when writing stuff out to detect &#dddd; and replace as
     appropriate. */

  private static final Pattern C_CharacterEntityPattern = Pattern.compile("\\&\\#[^;]+\\;");


  /****************************************************************************/
  /* Used to detect OTML header tags in output.  This gets a bit complicated ...
   * 
   * In the original USFM version of the processing, I'm sure we were generating
   * nested header styles in OTML (eg h5 within h4) and things were working.
   * 
   * However, they aren't working now -- it seems that you can't nest h5 within
   * h4, and I'm assuming the ban extends to not having nested headers generally.
   * Whether this is because OliveTree have changed things, whether it's
   * because the translations we dealt with previously had slightly different
   * tags in a slightly different order which masked the problem, or whether in
   * fact it's never worked, I don't know.
   * 
   * Anyway, it's kinda difficult to fix convincingly, because I don't actually
   * know for sure what kinds of tags are likely to hit me.  The solution I've
   * adopted is as follows ...
   * 
   * Headers are generated in two different ways.  In some cases they are
   * generated as a result of special processing here; and in other cases they
   * are generated as a result of "standard" tag processing.  In general,
   * I don't think the former is a problem, because with special processing I
   * have direct control over what's going on, and can alter the order in which
   * I do things to avoid the problem.
   * 
   * It's therefore the general processing which is the issue (and indeed it's
   * this which is giving rise to the problems we're seeing).
   * 
   * I attack this in the methods which output the opening and closing tags for
   * this standard processing.  In the method which handles opening tags, I
   * check to see if the opening text includes "<h*", where "*" represents a
   * digit.  If it does, I look at the m_PendingHeaders stack to see if we
   * are already in a header.  If so, the top entry of m_PendingHeaders tells
   * me the most recent header to have been opened, and I close it (and replace
   * the top entry with an empty string).  I then push the current header on
   * to the stack.
   * 
   * In the method which handles the closing tag, I look for </h*>.  If I find
   * it, I output it only if the top entry of the stack is a non-empty string
   * (recall that I've set it to an empty string if I've already output the
   * closing tag); and then I pop the stack.
   */

  private static final Pattern C_HeaderPatternOpen = Pattern.compile("\\<(h\\d)");
  private static final Pattern C_HeaderPatternClose = Pattern.compile("\\<\\/(h\\d)\\>");
  private Stack<String> m_PendingHeaders = new Stack<String>();


  /****************************************************************************/
  /* Styles which need text strings to be converted to XB form. */
  
  private static final String C_StylesNeedingXbReferences = ".io.iot.ip.is.r.";
  
  
  /****************************************************************************/
  /* Used to generate unique anchors. */

  private int m_AnchorNo = 0;


  /****************************************************************************/
  /* Determines whether or not we output hooks so we can use the proofer.
     Outputting them obviously means you can use the proofer, but makes the
     file appreciably bigger. */

  boolean m_IncludeHooksForProofer = false;


  /****************************************************************************/
  /* It's necessary to keep tabs on certain closing tags in order to handle
   * br's properly.
   */
  
  private String m_LastTag = "xx";
  
  
  /****************************************************************************/
  /* Length of text string processed by most recent hash_text processing. */

  int m_LengthOfMostRecentHashText = 0;


  /****************************************************************************/
  /* We need, in the main, to work with OTML book numbers, not UBS ones. */

  String m_OtmlBookNo = "";


  /****************************************************************************/
  /* Parsed text stringifier (vernacular, full names, not defaulted from
   * context). */
  
  ParsedTextOutputter m_ParsedTextOutputter_VernaculaNotDefaulted = new ParsedTextOutputter();

  
  /****************************************************************************/
  /* Stringifier which converts references to XB form. */
  
  private RefsAsXbOutputter m_RefsAsXbOutputter = new RefsAsXbOutputter();
  
  
  /****************************************************************************/
  /* Used to keep track fo which stringifier to use for text. */
  
  private Stack<ParsedTextOutputter> m_ParsedTextOutputterStack = new Stack<ParsedTextOutputter>();
  
  
  /****************************************************************************/
  /* I have an uneasy feeling that in some languages, a space may not actually
     be a space, so I make allowances for it to be something else. */

  String m_Space = " ";


  /****************************************************************************/
  /* Within certain tags, we need to needToSupress double slashes.  We simply keep
     a count of how many suppressions are currently in operation. */

  private int m_SuppressDoubleSlashes = 0;
}
